package com.zoe.onelink.mybatis.interceptor.impl;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import com.zoe.onelink.common.enumeration.ExceptionCodeEnum;
import com.zoe.onelink.common.exception.ApiException;
import com.zoe.onelink.mybatis.properties.OnelinkMybatisPluginProperties;
import com.zoe.onelink.util.PathMatchUtil;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

import java.util.Collection;

/**
 * <p>标题: Mybatis查询拦截器</p>
 * <p>描述: 该拦截器主要针对 1.没有分页的SQL 2.没有先执行count再分页查询的SQL</p>
 * <p>版权: Copyright (c) 2021</p>
 * <p>公司: 智业软件股份有限公司</p>
 *
 * @version: 1.0
 * @author: ruansheng
 * @date: 2021-10-20
 */
@Intercepts({
        @Signature(
                type = Executor.class,
                method = "query",
                args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
        @Signature(
                type = Executor.class,
                method = "query",
                args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
        @Signature(
                type = Executor.class,
                method = "queryCursor",
                args = {MappedStatement.class, Object.class, RowBounds.class})
})
public class MybatisQueryLimitInterceptor implements Interceptor {

    private final RowBounds rowBounds;
    private final OnelinkMybatisPluginProperties.QueryLimit queryLimitProp;

    public MybatisQueryLimitInterceptor(OnelinkMybatisPluginProperties mybatisProp) {
        OnelinkMybatisPluginProperties.QueryLimit queryLimitProp = mybatisProp.getQueryLimit();
        Assert.notNull(queryLimitProp, "Mybatis查询拦截器配置信息不能为空");
        this.queryLimitProp = queryLimitProp;
        int queryLimit = queryLimitProp.getMaxResultRows();
        // 这里offset不是分页中的页码,固定为0即可
        // 1. 分页SQL直接限制其分页数
        // 2. 非分页SQL,限制游标获取数量 (mybatis默认底层实现是基于游标方式从数据库服务端一条一条获取的,也就是说即便是全表查询,该数据也不会立即加载到当前服务的内存)
        // 游标业务逻辑参考: DefaultResultSetHandler#handleRowValuesForSimpleResultMap
        this.rowBounds = queryLimit > 0 ? new RowBounds(0, queryLimit + 1) : null;
    }

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = invocation.getArgs();
        // 返回结果类型
        Class<?> returnType = invocation.getMethod().getReturnType();
        // 方法名全路径,例: com.zoe.xxx.selectPage
        String methodId = ((MappedStatement) args[0]).getId();
        // 如果返回结果为集合,进行结果集阈值拦截
        if (this.rowBounds == null || !Collection.class.isAssignableFrom(returnType) || !this.isMatched(methodId)) {
            return invocation.proceed();
        }
        // 覆盖原有的RowBounds
        args[2] = this.rowBounds;
        // 获取结果集
        Object result = invocation.proceed();
        // 如果处理策略为抛出异常的方式,由于前面设置了rowBounds.limit,所以当该值为false时,返回结果的最大行数即为rowBounds.limit
        if (this.queryLimitProp.isThrowIfOverLimit()
                && result instanceof Collection
                // 如果返回数量与阈值一致,说明数据库实际行数超过规定的最大行数 (因为rowBounds.limit = queryLimit + 1)
                && ((Collection<?>) result).size() == this.rowBounds.getLimit()) {
            throw ApiException.createEx(ExceptionCodeEnum.DB_RESULT_OVER_LIMIT, this.queryLimitProp.getMaxResultRows());
        }
        return result;
    }

    /**
     * 方法匹配
     */
    private boolean isMatched(String methodId) {
        // 未配置默认生效
        if (CollUtil.isEmpty(this.queryLimitProp.getIncludeMethods()) && CollUtil.isEmpty(this.queryLimitProp.getExcludeMethods())) {
            return true;
        }
        return PathMatchUtil.isPermit(methodId, this.queryLimitProp.getIncludeMethods(), this.queryLimitProp.getExcludeMethods(),
                true, true, "SQL_LIMIT");
    }
}
